﻿using RockBus.Configuration;
using RockBus.DataContracts;
using RockBus.Logging.Interfaces;
using RockBus.ServiceModel;
using RockBus.Transport.Wcf;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Threading.Tasks;

namespace RockBus.ServiceBus
{
    public class ServiceBusEnvironment : IDisposable
    {
        private const int DefaultWaitTimeoutMs = 30000;

        public ServiceBusEnvironment(ILogger logger, EndpointInfo rockBusEndpoint = null)
        {
            this.Logger = logger;
            if (rockBusEndpoint == null)
            {
                rockBusEndpoint = GetRockBusEndpoint();
            }
            this.ClientTransport = new ClientTransport(logger, rockBusEndpoint);
        }

        public ILogger Logger { get; private set; }

        public ClientTransport ClientTransport { get; private set; }

        private readonly ConcurrentDictionary<string, ServiceHostBase> ServiceHosts = new ConcurrentDictionary<string, ServiceHostBase>();

        public EndpointInfo GetRockBusEndpoint()
        {
            if ((null == ServiceBusClientConfigurationSection.Instance) || (null == ServiceBusClientConfigurationSection.Instance.RockBusEndpoint))
            {
                var exc = new ConfigurationErrorsException("The ServiceBus RockBusEndpoint was not configured. Pls. refer to RockBus.Configuration.ServiceBusClientConfigurationSection for the configuration schemas and documentation.");
                this.Logger.Log(MethodBase.GetCurrentMethod(), exc);
                throw exc;
            }

            var rockBusEndpoint = ServiceBusClientConfigurationSection.Instance.RockBusEndpoint.CreateEndpointInfo();
            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "Using RockBus Endpoint {0}", rockBusEndpoint);
            return rockBusEndpoint;
        }

        public void Dispose()
        {
            Parallel.ForEach(this.ServiceHosts.Values, (serviceHost) =>
            {
                this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "Closing ServiceHost {0}", serviceHost.Description);
                serviceHost.TryCloseAsync(DefaultWaitTimeoutMs);
            });
        }

        #region ServiceHost

        /// <summary>
        /// Creates and opens a service host that will listen on the specified endpoints, and will hand over any message processing to the specified messageHandler.
        /// </summary>
        /// <param name="messageHandler">The instance of IMessageHandler that will handle any messages received by the ServiceHost.</param>
        /// <param name="endpointInfo">The endpoint (containing relative uri) that the ServiceHost should listen on.</param>
        /// <returns>true if the ServiceHost was created and opened. Otherwise, false.</returns>
        public Task<bool> TryCreateAndOpenServiceHostAsync(IMessageHandler messageHandler, EndpointInfo endpointInfo, int timeoutMs = DefaultWaitTimeoutMs)
        {
            var clientService = new SubscriberService(messageHandler);
            var serviceHost = new SubscriberServiceHost(this.Logger, clientService, endpointInfo);
            if (!this.ServiceHosts.TryAdd(messageHandler.Id, serviceHost))
            {
                return Task.FromResult(false);
            }

            return serviceHost.TryOpenAsync(timeoutMs).ContinueWith((t) =>
            {
                this.Logger.LogDebug(MethodBase.GetCurrentMethod(), serviceHost.GetHostDetails());
                if (t.IsCompleted)
                {
                    return t.Result;
                }
                else
                {
                    this.Logger.Log(MethodBase.GetCurrentMethod(), t.Exception);
                    return false;
                }
            });
        }

        /// <summary>
        /// Closes the ServiceHost that's associated with the MessageHandlerId.
        /// </summary>
        /// <param name="messageHandlerId">The id of the MessageHandler, for which the ServiceHost should be closed.</param>
        /// <param name="timeoutMs">The timeout to apply to the Close method.</param>
        /// <returns>true if the ServiceHost was closed; otherwise, false.</returns>
        public Task<bool> TryCloseServiceHost(string messageHandlerId, int timeoutMs = DefaultWaitTimeoutMs)
        {
            ServiceHostBase serviceHost;
            if (!this.ServiceHosts.TryRemove(messageHandlerId, out serviceHost))
            {
                return Task.FromResult(false);
            }

            return serviceHost.TryCloseAsync(timeoutMs).ContinueWith((t) =>
            {
                this.Logger.LogDebug(MethodBase.GetCurrentMethod(), serviceHost.GetHostDetails(ServiceHostExtensions.ClosingDescription));
                return t.Result;
            });
        }

        #endregion ServiceHost

        #region Subscribers

        public Task ConfigureSubscribersAsync()
        {
            return this.AddOrUpdateSubscribersAsync(new AddOrUpdateSubscribersRequest(ServiceBusClientConfigurationSection.Instance.Subscribers.CreateSubscriberConfiguration()));
        }

        public Task UnconfigureSubscribersAsync()
        {
            return this.RemoveSubscribersAsync(new RemoveSubscribersRequest(ServiceBusClientConfigurationSection.Instance.Subscribers.CreateSubscriberConfiguration().EvaluationElementDictionary.Values.Select((si) => si.Id)));
        }

        /// <summary>
        /// Adds or updates the Subscribers Configuration to RockBus
        /// </summary>
        /// <param name="request">Contains the Subscribers configuration.</param>
        /// <returns></returns>
        public Task AddOrUpdateSubscribersAsync(AddOrUpdateSubscribersRequest request)
        {
            return this.SendConfigurationServiceMessageAsync<AddOrUpdateSubscribersRequest>(request, StringConstants.AddOrUpdateSubscribersAction);
        }

        /// <summary>
        /// Removes all the subscriptions for the specified Subscriber.
        /// </summary>
        /// <param name="request">Contains the SubscriberId, for which all subscriptions should be removed.</param>
        /// <returns></returns>
        public Task RemoveSubscriptionsAsync(RemoveSubscriptionsRequest request)
        {
            return this.SendConfigurationServiceMessageAsync<RemoveSubscriptionsRequest>(request, StringConstants.RemoveSubscriptionsAction);
        }

        /// <summary>
        /// Removes all Subscription Configuration for the specified Subscriber Ids.
        /// </summary>
        /// <param name="request">Contains a collection of Subscriber Ids.</param>
        /// <returns></returns>
        public Task RemoveSubscribersAsync(RemoveSubscribersRequest request)
        {
            return this.SendConfigurationServiceMessageAsync<RemoveSubscribersRequest>(request, StringConstants.RemoveSubscribersAction);
        }

        /// <summary>
        ///  Removes the Subscriber Configuration for all subscribers.
        /// </summary>
        /// <returns></returns>
        public Task RemoveAllSubscribersAsync()
        {
            var request = new RemoveAllSubscribersRequest();
            return this.SendConfigurationServiceMessageAsync<RemoveAllSubscribersRequest>(request, StringConstants.RemoveAllSubscribersAction);
        }

        #endregion Subscribers

        #region Publishers

        public Task ConfigurePublishersAsync()
        {
            return this.AddOrUpdatePublishersAsync(new AddOrUpdatePublishersRequest(ServiceBusClientConfigurationSection.Instance.Publishers.CreatePublisherConfiguration()));
        }

        public Task UnconfigurePublishersAsync()
        {
            return this.RemovePublishersAsync(new RemovePublishersRequest(ServiceBusClientConfigurationSection.Instance.Subscribers.CreateSubscriberConfiguration().EvaluationElementDictionary.Values.Select((si) => si.Id)));
        }

        /// <summary>
        /// Adds or updates the Publishers Configuration to RockBus
        /// </summary>
        /// <param name="request">Contains the Publishers configuration.</param>
        /// <returns></returns>
        public Task AddOrUpdatePublishersAsync(AddOrUpdatePublishersRequest request)
        {
            return this.SendConfigurationServiceMessageAsync<AddOrUpdatePublishersRequest>(request, StringConstants.AddOrUpdatePublishersAction);
        }

        /// <summary>
        /// Removes all the receive ports for the specified Publisher.
        /// </summary>
        /// <param name="request">Contains the PublisherId, for which all receive ports should be removed.</param>
        /// <returns></returns>
        public async Task<AddOrUpdatePublishersResponse> AddOrUpdatePublishersWithResponseAsync(AddOrUpdatePublishersRequest request)
        {
            var requestMessageConverter = TypedMessageConverter.Create(
                                typeof(AddOrUpdatePublishersRequest),
                                StringConstants.AddOrUpdatePublishersAction,
                                StringConstants.RockBusNamespace);
            var requestMessage = requestMessageConverter.ToMessage(request, this.ClientTransport.MessageVersion);

            var responseMessage = await this.ClientTransport.SendRequestAsync(requestMessage, null);

            var responseMessageConverter = TypedMessageConverter.Create(
                                typeof(AddOrUpdatePublishersResponse),
                                StringConstants.AddOrUpdatePublishersResponseAction,
                                StringConstants.RockBusNamespace);
            return responseMessageConverter.FromMessage(responseMessage) as AddOrUpdatePublishersResponse;
        }

        /// <summary>
        /// Removes all the receive ports for the specified Publisher.
        /// </summary>
        /// <param name="request">Contains the PublisherId, for which all receive ports should be removed.</param>
        /// <returns></returns>
        public Task RemoveReceivePortsAsync(RemoveReceivePortsRequest request)
        {
            return this.SendConfigurationServiceMessageAsync<RemoveReceivePortsRequest>(request, StringConstants.RemoveReceivePortsAction);
        }

        /// <summary>
        /// Removes all Publisher Configuration for the specified Publisher Ids.
        /// </summary>
        /// <param name="request">Contains a collection of Publisher Ids.</param>
        /// <returns></returns>
        public Task RemovePublishersAsync(RemovePublishersRequest request)
        {
            return this.SendConfigurationServiceMessageAsync<RemovePublishersRequest>(request, StringConstants.RemovePublishersAction);
        }

        /// <summary>
        ///  Removes the Publisher Configuration for all publishers.
        /// </summary>
        /// <returns></returns>
        public Task RemoveAllPublishersAsync()
        {
            var request = new RemoveAllPublishersRequest();
            return this.SendConfigurationServiceMessageAsync<RemoveAllPublishersRequest>(request, StringConstants.RemoveAllPublishersAction);
        }

        #endregion Publishers

        /// <summary>
        /// This works because the Operation Action and the WrapperNamespaces are identical
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="messageContract"></param>
        /// <param name="messageNamespace"></param>
        private Task SendConfigurationServiceMessageAsync<TRequest>(TRequest messageContract, string action)
        {
            var requestMessageConverter = TypedMessageConverter.Create(
                             messageContract.GetType(),
                             action,
                             StringConstants.RockBusNamespace);
            var requestMessage = requestMessageConverter.ToMessage(messageContract, this.ClientTransport.MessageVersion);
            return this.ClientTransport.SendRequestAsync(requestMessage);
        }
    }
}